package lang.asm;

import editor.NotImplementedException;
import lang.Saver;
import model.Project;
import model.Segment;
import model.code.*;
import model.code.line.Line;
import model.symbol.Constant;
import model.symbol.LocalVariable;
import model.symbol.StructureMember;
import observer.ObservedList;

import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;

/**
 * This saves assembly language files.
 * @author Neil Dickson
 */
public class ASMSave extends Saver {
	public static final ASMSave instance = new ASMSave();

	private ASMSave() {

	}

	public void saveFile(CodeFile file,PrintStream output) throws IOException {
		if (file.isIncludeFile()) {
			output.println(".nolist");
		}
		output.print(ASMDoc.instance.createDocComment(file));
		output.println();
		// NOTE: 64-bit MASM doesn't support the following directives, so leave them out for compatibility 
		if (file.isMainFile() && file.getProject().getDefaultSegment(Segment.CODE).getnBits().get()!=64) {
			output.println(".686p");
			output.println(".xmm");
			output.println(".model flat,stdcall");
			output.println();
		}
		if (file.isMainFile()) {
			Project project = file.getProject();
			ObservedList<Project.ExternalInclude> libraryIncludes = project.getLibraryIncludes();
			if (libraryIncludes!=null && libraryIncludes.size()>0) {
				for (Project.ExternalInclude include : libraryIncludes) {
					output.println("include\t\t"+include.getName()+".inc");
				}
				output.println();
				for (Project.ExternalInclude include : libraryIncludes) {
					output.println("includelib\t"+include.getName()+".lib");
				}
				output.println();
			}
			ObservedList<Project.ExternalInclude> importedIncludes = project.getImportedIncludes();
			if (importedIncludes!=null && importedIncludes.size()>0) {
				for (Project.ExternalInclude include : importedIncludes) {
					output.println("include\t\t"+include.getName());
				}
				output.println();
			}
		}

		ArrayList<CodeFileChunk> content = file.getContent();
		boolean isCode = false;
		boolean isData = false;
		for (CodeFileChunk chunk : content) {
			if (chunk instanceof Function && !isCode) {
				output.println(".code");
				isCode = true;
				isData = false;
			}
			else if (chunk instanceof GlobalVariable && !isData) {
				output.println(".data");
				isCode = false;
				isData = true;
			}
			else if (chunk instanceof Section && ((Section)chunk).getSegmentName().length()>0) {
				isCode = false;
				isData = false;
			}
			saveChunk(chunk,output);
			output.println();
		}

		if (file.isIncludeFile()) {
			output.println(".list");
		}
		if (file.isMainFile()) {
			output.print("end");
			Function mainEntryPoint = file.getProject().getEntryPoint();
			if (mainEntryPoint!=null || !file.getProject().getEntryPointLine().equals(Project.DEFAULT_ENTRY_POINT_NAME)) {
				output.println(" "+file.getProject().getEntryPointLine());
			}
		}
	}

	public void saveFunction(Function function, PrintStream output) throws IOException {
		output.print(ASMDoc.instance.createDocComment(function));
		if (function.getOffset().length()>0) {
			output.println("org\t"+function.getOffset());
		}
		if (function.getAlignment().get()!=CodeFileChunk.DEFAULT_ALIGNMENT_MARKER) {
			// NOTE: Although "align 1" may seem redundant, the default alignment for Functions may not be 1, so without this, no variables stay aligned to 1 byte.
			output.println("align\t"+function.getAlignment());
		}

		output.print(function.getName());
		boolean isMacro = function.getCallingConvention().equals(Function.INLINE_TEXT);
		if (!isMacro) {
			output.print("\tproc");
			if (function.getCallingConvention().equals(Function.FAR_TEXT) || function.getCallingConvention().equals(Function.C_TEXT)) {
				output.print("\t");
				output.print(function.getCallingConvention());
			}
			else if (function.getCallingConvention().length()>0) {
				output.print("\t<");
				output.print(function.getCallingConvention());
				output.print(">");
			}
		}
		else {
			output.print("\tmacro");
		}
		ArrayList<LocalVariable> parameters = function.getParameters();
		ArrayList<LocalVariable> localVariables = function.getLocalVariables();
		boolean hasStackParameters = false;
		for (LocalVariable parameter : parameters) {
			if (!parameter.getType().toString().equalsIgnoreCase(Function.AUTOMATIC_PARAM_TYPE_TEXT)) {
				// Tab before the first parameter and comma before all others
				output.print(hasStackParameters?',':'\t');
				output.print(parameter.getName()+":"+parameter.getType());
				hasStackParameters = true;
			}
		}
		output.println();
		// Write out local variables in opposite order because they appear in opposite order in memory
		for (int i=localVariables.size()-1;i>=0;--i) {
			LocalVariable variable = localVariables.get(i);
			output.println(" LOCAL\t"+variable.getName()+":"+variable.getType());
		}

		for (Line line : function.getCode()) {
			output.println(line);
		}
		if (!isMacro) {
			output.println(function.getName()+"\tendp");
		}
		else {
			output.println("endm");
		}
	}

	public void saveVariable(GlobalVariable variable, PrintStream output) throws IOException {
		output.print(ASMDoc.instance.createDocComment(variable));
		if (variable.getOffset().length()!=0) {
			output.println("org\t"+variable.getOffset());
		}
		if (variable.getAlignment().get()!=CodeFileChunk.DEFAULT_ALIGNMENT_MARKER) {
			// NOTE: Although "align 1" may seem redundant, the default alignment for GlobalVariables is currently 4, so without this, no variables stay aligned to 1 byte.
			output.println("align\t"+variable.getAlignment());
		}

		output.print(variable.getName()+"\t");
		ObservedList<GlobalVariable.Piece> pieces = variable.getPieces();
		if (!pieces.isEmpty()) {
			output.println(pieces.get(0).getType()+"\t"+pieces.get(0).getInitialValue());
		}
		if (pieces.size()>1) {
			int numTabs = (variable.getName().length()+4)/4;
			String tabs = "\t";	// There must be at least one tab
			while (tabs.length() < numTabs) {
				tabs += "\t";
			}
			for (int i=1;i<pieces.size();++i) {
				output.println(tabs+pieces.get(i).getType()+"\t"+pieces.get(i).getInitialValue());
			}
		}
	}

	public void saveStructure(Structure structure, PrintStream output) throws IOException {
		output.print(ASMDoc.instance.createDocComment(structure));
		output.println(structure.getName()+"\tstruct");
		ArrayList<StructureMember> members = structure.getMembers();
		int maxNameSize = 0;
		int maxTypeSize = 0;
		for (StructureMember member : members) {
			maxNameSize = Math.max(maxNameSize, member.getName().length());
			maxTypeSize = Math.max(maxTypeSize, member.getType().length());
		}
		maxNameSize = (maxNameSize+4)&(~3);
		maxTypeSize = (maxTypeSize+4)&(~3);

		for (StructureMember member : members) {
			output.print("\t"+member.getName()+"\t");	// Always at least one tab after name
			int size = (member.getName().length() + 4)&(~3);
			while (size < maxNameSize) {
				output.print('\t');
				size += 4;	// Can do this without the "&(~3)" because already one tab at start
			}

			output.print(member.getType()+"\t");		// Always at least one tab after type
			size = (member.getType().length() + 4)&(~3);
			while (size < maxTypeSize) {
				output.print('\t');
				size += 4;	// Can do this without the "&(~3)" because already one tab at start
			}
			output.println(member.getInitialValue());
		}
		output.println(structure.getName()+"\tends");
	}

	public void saveConstants(Constants constants, PrintStream output) throws IOException {
		output.print(ASMDoc.instance.createDocComment(constants));
		ArrayList<Constant> members = constants.getMembers();
		int maxSize = 0;
		for (Constant member : members) {
			maxSize = Math.max(maxSize, member.getName().length());
		}
		maxSize = (maxSize+4)&(~3);
		for (Constant member : members) {
			output.print(member.getName()+"\t");	// Always at least one tab
			int size = (member.getName().length() + 4)&(~3);
			while (size < maxSize) {
				output.print('\t');
				size += 4;	// Can do this without the "&(~3)" because already one tab at start
			}
			output.print("equ\t"+constants.getEnumeratedValue(member));
			// If the value is generated by the enumeration, mark it, so that this is known upon reloading.
			if (constants.isEnumeration() && member.getValue().length()==0) {
				output.print("\t;"+ASM.ENUMERATION_VALUE_MARKER);
			}
			output.println();
		}
	}

	public void saveSection(Section section, PrintStream output) throws IOException {
		output.print(ASMDoc.instance.createDocComment(section));
		String segmentName = section.getSegmentName().toString();
		boolean isInSegment = (segmentName.length()!=0);
		boolean isDefaultSegment = true;
		ArrayList<CodeFileChunk> content = section.getContent();
		if (isInSegment) {
			Project project = section.getProject();
			Segment segment = project.getSegment(segmentName);
			if (segment==project.getDefaultSegment(Segment.CODE)) {
				output.println(ASM.CODE_SEGMENT);
			}
			else if (segment==project.getDefaultSegment(Segment.INIT_DATA)) {
				output.println(ASM.INIT_DATA_SEGMENT);
			}
			else if (segment==project.getDefaultSegment(Segment.READ_ONLY_DATA)) {
				output.println(ASM.READ_ONLY_DATA_SEGMENT);
			}
			else if (segment==project.getDefaultSegment(Segment.UNINIT_DATA)) {
				output.println(ASM.UNINIT_DATA_SEGMENT);
			}
			else {
				output.println(segmentName+"\tsegment\tuse"+segment.getnBits()+" align("+segment.getAlignment().get()+")");
				isDefaultSegment = false;
			}
			output.println();
		}
		boolean isCode = false;
		boolean isData = false;
		for (CodeFileChunk chunk : content) {
			if (!isInSegment) {
				if ((chunk instanceof Function) && !isCode) {
					output.println(ASM.CODE_SEGMENT);
					isCode = true;
					isData = false;
				}
				else if ((chunk instanceof GlobalVariable) && !isData) {
					output.println(ASM.INIT_DATA_SEGMENT);
					isCode = false;
					isData = true;
				}
			}
			saveChunk(chunk,output);
			output.println();
		}
		if (isInSegment && !isDefaultSegment) {
			output.println(segmentName+"\tends");
			output.println();
		}
	}

	public void savePrototypes(Prototypes prototypes, PrintStream output) throws IOException {
		ArrayList<Function> functions = prototypes.getFunctions();
		int maxSize = 0;
		for (Function function : functions) {
			maxSize = Math.max(maxSize, function.getName().length());
		}
		maxSize = (maxSize+4)&(~3);
		for (Function function : functions) {
			ObservedList<LocalVariable> parameters = function.getParameters();
			output.print(function.getName()+"\t");	// Always at least one tab
			int size = (function.getName().length() + 4)&(~3);
			while (size < maxSize) {
				output.print('\t');
				size += 4;	// Can do this without the "&(~3)" because already one tab at start
			}
			output.print("proto");
			if (parameters.size()>0) {
				boolean hasStackParameters = false;
				for (LocalVariable parameter : parameters) {
					if (!parameter.getType().toString().equalsIgnoreCase(Function.AUTOMATIC_PARAM_TYPE_TEXT)) {
						// Tab before the first parameter and comma before all others
						output.print(hasStackParameters?',':'\t');
						output.print(parameter.getName()+":"+parameter.getType());
						hasStackParameters = true;
					}
				}
			}
			output.println();
		}
	}

	public void savePreprocessorLine(PreprocessorLine line, PrintStream output) throws IOException {
		throw new NotImplementedException();
	}

}
